 /*******************************************************************************
  * Copyright (c) 2004, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.internal;

 import java.util.Iterator ;
 import java.util.List ;

 import org.eclipse.jface.util.Geometry;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.graphics.Cursor;
 import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.graphics.Rectangle;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.ui.internal.dnd.DragBorder;
 import org.eclipse.ui.internal.dnd.DragUtil;
 import org.eclipse.ui.internal.dnd.IDragOverListener;
 import org.eclipse.ui.internal.dnd.IDropTarget;
 import org.eclipse.ui.internal.dnd.IDropTarget2;
 import org.eclipse.ui.internal.layout.IWindowTrim;
 import org.eclipse.ui.internal.layout.LayoutUtil;
 import org.eclipse.ui.internal.layout.TrimDescriptor;
 import org.eclipse.ui.internal.layout.TrimLayout;
 import org.eclipse.ui.internal.layout.TrimToolBarBase;

 /**
  */
 /*package*/class TrimDropTarget implements IDragOverListener {
     
     private final class ActualTrimDropTarget implements IDropTarget2 {
         public IWindowTrim draggedTrim;
         
         // tracking parameters
 private DragBorder border = null;
         private int dockedArea;
         
         // Holder for the position of trim that is 'floating' with the cursor
 private int cursorAreaId;
         private int initialAreaId;
         private IWindowTrim initialInsertBefore;
         private Rectangle initialLocation;

         /**
          * Constructor
          */
         private ActualTrimDropTarget() {
             super();

             draggedTrim = null;
             dockedArea = SWT.NONE;
             
             initialAreaId = SWT.NONE;
             initialInsertBefore = null;
         }
         
         /**
          * This method is used to delineate separate trims dragging events. The -first- drag
          * event will set this and then it will remain constant until the drag gesture is done;
          * either by dropping or escaping. Once the gesture is finished the trim value is set
          * back to 'null'.
          *
          * @param trim The trim item currently being dragged.
          */
         public void startDrag(IWindowTrim trim) {
             // Are we starting a new drag?
 if (draggedTrim != trim) {
                 // remember the dragged trim
 draggedTrim = trim;
                 
                 // Remember the location that we were in initially so we
 // can go back there on an cancel...
 initialAreaId = layout.getTrimAreaId(draggedTrim.getControl());
                 
                 // Determine who we were placed 'before' in the trim
 initialInsertBefore = getInsertBefore(initialAreaId, draggedTrim);
                 
                 // Remember the location that the control used to be at for animation purposes
 initialLocation = DragUtil.getDisplayBounds(draggedTrim.getControl());
                                 
                 // The dragged trim is always initially docked
 dockedArea = initialAreaId;
             }
         }
                 
         /**
          * Determine the trim area from the point. To avoid clashing at the 'corners' due to extending the trim area's
          * rectangles we first ensure that the point is not actually -within- a trim area before we check the extended
          * rectangles.
          *
          * @param pos The current cursor pos
          * @return the Trim area that the cursor is in or SWT.NONE if the point is not in an area
          */
         private int getTrimArea(Point pos) {
             // First, check if we're actually -within- a trim area (i.e. no boundary extensions)
 int areaId = getTrimArea(pos, 0);
             
             // If we are not inside a trim area...are we 'close' to one?
 if (areaId == SWT.NONE) {
                 areaId = getTrimArea(pos, TrimDragPreferences.getThreshold());
             }
             
             // not inside any trim area
 return areaId;
         }
         
         /**
          * Checks the trims areas against the given position. Each trim area is 'extended' into
          * the workbench page by the value of <code>extendedBoundaryWidth</code> before the checking
          * takes place.
          *
          * @param pos The point to check against
          * @param extendedBoundaryWidth The amount to extend the trim area's 'inner' edge by
          *
          * @return The trim area or SWT.NONE if the point is not within any extended trim area's rect.
          */
         private int getTrimArea(Point pos, int extendedBoundaryWidth) {
             int[] areaIds = layout.getAreaIds();
             for (int i = 0; i < areaIds.length; i++) {
                 Rectangle trimRect = layout.getTrimRect(windowComposite, areaIds[i]);
                 trimRect = Geometry.toControl(windowComposite, trimRect);

                 // Only check 'valid' sides
 if ( (areaIds[i] & getValidSides()) != SWT.NONE) {
                     // TODO: more confusion binding 'areaIds' to SWT 'sides'
 switch (areaIds[i]) {
                     case SWT.LEFT:
                         trimRect.width += extendedBoundaryWidth;
                         
                         if (pos.y >= trimRect.y &&
                             pos.y <= (trimRect.y+trimRect.height) &&
                             pos.x <= (trimRect.x+trimRect.width)) {
                             return areaIds[i];
                         }
                         break;
                     case SWT.RIGHT:
                         trimRect.x -= extendedBoundaryWidth;
                         trimRect.width += extendedBoundaryWidth;
                         
                         if (pos.y >= trimRect.y &&
                             pos.y <= (trimRect.y+trimRect.height) &&
                             pos.x >= trimRect.x) {
                             return areaIds[i];
                         }
                         break;
                     case SWT.TOP:
                         trimRect.height += extendedBoundaryWidth;
                         
                         if (pos.x >= trimRect.x &&
                             pos.x <= (trimRect.x+trimRect.width) &&
                             pos.y <= (trimRect.y+trimRect.height)) {
                             return areaIds[i];
                         }
                         break;
                     case SWT.BOTTOM:
                         trimRect.y -= extendedBoundaryWidth;
                         trimRect.height += extendedBoundaryWidth;
                         
                         if (pos.x >= trimRect.x &&
                             pos.x <= (trimRect.x+trimRect.width) &&
                             pos.y >= trimRect.y) {
                             return areaIds[i];
                         }
                         break;
                     }
                 }
             }
             
             // not inside any trim area
 return SWT.NONE;
         }
         
         /**
          * Determine the window trim that the currently dragged trim should be inserted
          * before.
          * @param areaId The area id that is being checked
          * @param pos The position used to determine the correct insertion trim
          * @return The trim to 'dock' the draggedTrim before
          */
         private IWindowTrim getInsertBefore(int areaId, Point pos) {
             boolean isHorizontal = (areaId == SWT.TOP) || (areaId == SWT.BOTTOM);
             
             // Walk the trim area and return the first one that the positon
 // is 'after'.
 List tDescs = layout.getTrimArea(areaId).getDescriptors();
             for (Iterator iter = tDescs.iterator(); iter.hasNext();) {
                 TrimDescriptor desc = (TrimDescriptor) iter.next();
                 
                 // Skip ourselves
 if (desc.getTrim() == draggedTrim) {
                     continue;
                 }
                 
                 // Now, check
 Rectangle bb = desc.getCache().getControl().getBounds();
                 Point center = Geometry.centerPoint(bb);
                 if (isHorizontal) {
                     if (pos.x < center.x) {
                         return desc.getTrim();
                     }
                 }
                 else {
                     if (pos.y < center.y) {
                         return desc.getTrim();
                     }
                 }
             }
             
             return null;
         }
         
         /**
          * Returns the trim that is 'before' the given trim in the given area
          *
          * @param areaId The areaId of the trim
          * @param trim The trim to find the element after
          *
          * @return The trim that the given trim is 'before'
          */
         private IWindowTrim getInsertBefore(int areaId, IWindowTrim trim) {
             List tDescs = layout.getTrimArea(areaId).getDescriptors();
             for (Iterator iter = tDescs.iterator(); iter.hasNext();) {
                 TrimDescriptor desc = (TrimDescriptor) iter.next();
                 if (desc.getTrim() == trim) {
                     if (iter.hasNext()) {
                         desc = (TrimDescriptor) iter.next();
                         return desc.getTrim();
                     }
                     return null;
                 }
             }
             
             return null;
         }
         
         /**
          * Recalculates the drop information based on the current cursor pos.
          *
          * @param pos The cursor position
          */
         public void track(Point pos) {
             // Convert the mouse positon into 'local' coords
 Rectangle r = new Rectangle(pos.x, pos.y, 1,1);
             r = Geometry.toControl(windowComposite, r);
             pos.x = r.x;
             pos.y = r.y;
                         
             // Are we 'inside' a trim area ?
 cursorAreaId = getTrimArea(pos);

             // Provide tracking for the appropriate 'mode'
 if (cursorAreaId != SWT.NONE) {
                 trackInsideTrimArea(pos);
             } else {
                 trackOutsideTrimArea(pos);
             }
         }
        
         /**
          * Perform the feedback used when the cursor is 'inside' a particular trim area.
          * The current implementation will place the dragged trim into the trim area at
          * the location determined by the supplied point.
          *
          * @param pos The point to use to determine where in the trim area the dragged trim
          * should be located.
          */
         private void trackInsideTrimArea(Point pos) {
             // Where should we be?
 int newArea = getTrimArea(pos);
             IWindowTrim newInsertBefore = getInsertBefore(newArea, pos);

             // if we're currently undocked then we should dock
 boolean shouldDock = dockedArea == SWT.NONE;
             
             // If we're already docked then only update if there's a change in area or position
 if (dockedArea != SWT.NONE) {
                 // Where are we now?
 IWindowTrim curInsertBefore = getInsertBefore(dockedArea, draggedTrim);
                 
                 // If we're already docked we should only update if there's a change
 shouldDock = dockedArea != newArea || curInsertBefore != newInsertBefore;
             }
             
             // Do we have to do anything?
 if (shouldDock) {
                 // (Re)dock the trim in the new location
 dock(newArea, newInsertBefore);
             }
         }
         
         /**
          * Provide the dragging feedback when the cursor is -not- explicitly inside
          * a particular trim area.
          *
          */
         private void trackOutsideTrimArea(Point pos) {
             // If we -were- docked then undock
 if (dockedArea != SWT.NONE) {
                 undock();
             }
             
             border.setLocation(pos, SWT.BOTTOM);
         }
                 
         /**
          * Return the set of valid sides that a piece of trim can be docked on. We
          * arbitrarily extend this to include any areas that won't cause a change in orientation
          *
          * @return The extended drop 'side' set
          */
         private int getValidSides() {
             int result = draggedTrim.getValidSides();
             if (result == SWT.NONE) {
                 return result;
             }

             // For now, if we can dock...we can dock -anywhere-
 return SWT.TOP | SWT.BOTTOM | SWT.LEFT | SWT.RIGHT;
         }

         /**
          * The user either cancelled the drag or tried to drop the trim in an invalid
          * area...put the trim back in the last location it was in
          */
         private void redock() {
             // Since the control might move 'far' we'll provide an animation
 Rectangle startRect = DragUtil.getDisplayBounds(draggedTrim.getControl());
             RectangleAnimation animation = new RectangleAnimation(
                     windowComposite.getShell(), startRect, initialLocation, 300);
             animation.schedule();

             dock(initialAreaId, initialInsertBefore);
         }
         
         /* (non-Javadoc)
          * @see org.eclipse.ui.internal.dnd.IDropTarget#drop()
          */
         public void drop() {
             // If we aren't docked then restore the initial location
 if (dockedArea == SWT.NONE) {
                 redock();
             }
         }

         /**
          * Remove the trim frmo its current 'docked' location and attach it
          * to the cursor...
          */
         private void undock() {
             // Remove the trim from the layout
 layout.removeTrim(draggedTrim);
             LayoutUtil.resize(draggedTrim.getControl());
             
             // Re-orient the widget to its -original- side and size
 draggedTrim.dock(initialAreaId);
             draggedTrim.getControl().setSize(initialLocation.width, initialLocation.height);
             
             // Create a new dragging border onto the dragged trim
 // Special check for TrimPart...should be generalized
 boolean wantsFrame = !(draggedTrim instanceof TrimToolBarBase);
             border = new DragBorder(windowComposite, draggedTrim.getControl(), wantsFrame);

             dockedArea = SWT.NONE;
         }
         
         /**
          * Return the 'undocked' trim to its previous location in the layout
          */
         private void dock(int areaId, IWindowTrim insertBefore) {
             // remove the drag 'border'
 if (border != null) {
                 border.dispose();
                 border = null;
             }
             
             // Update the trim's orientation if necessary
 draggedTrim.dock(areaId);

             // Add the trim into the layout
 layout.addTrim(areaId, draggedTrim, insertBefore);
             LayoutUtil.resize(draggedTrim.getControl());
             
             // Remember the area that we're currently docked in
 dockedArea = areaId;
         }
             
         /* (non-Javadoc)
          * @see org.eclipse.ui.internal.dnd.IDropTarget#getCursor()
          */
         public Cursor getCursor() {
             // If the trim isn't docked then show the 'no smoking' sign
 if (cursorAreaId == SWT.NONE) {
                 return windowComposite.getDisplay().getSystemCursor(SWT.CURSOR_NO);
             }
             
             // It's docked; show the four-way arrow cursor
 return windowComposite.getDisplay().getSystemCursor(SWT.CURSOR_SIZEALL);
         }

         /* (non-Javadoc)
          * @see org.eclipse.ui.internal.dnd.IDropTarget#getSnapRectangle()
          */
         public Rectangle getSnapRectangle() {
             // TODO: KLUDGE!! We don't want to show -any- snap rect
 // but Tracker won't allow that so place it where it won't be visible
 return new Rectangle(100000, 0,0,0);
         }

         /* (non-Javadoc)
          * @see org.eclipse.ui.internal.dnd.IDropTarget2#dragFinished(boolean)
          */
         public void dragFinished(boolean dropPerformed) {
             // If we didn't perform a drop then restore the original position
 if (!dropPerformed && dockedArea == SWT.NONE) {
                 // Force the dragged trim back into its original position...
 redock();
             }
             
             // Set the draggedTrim to null. This indicates that we're no longer
 // dragging the trim. The first call to the TrimDropTarget's 'drag' method
 // will reset this the next time a drag starts.
 draggedTrim = null;
         }
     }
     
     private ActualTrimDropTarget dropTarget;
     
     private TrimLayout layout;
     private Composite windowComposite;

     /**
      * Create a new drop target capable of accepting IWindowTrim items
      *
      * @param someComposite The control owning the TrimLayout
      * @param theWindow the workbenchWindow
      */
     public TrimDropTarget(Composite someComposite, WorkbenchWindow theWindow) {
         layout = (TrimLayout) someComposite.getLayout();
         windowComposite = someComposite;

         // Create an instance of a drop target to use
 dropTarget = new ActualTrimDropTarget();
     }

     /* (non-Javadoc)
      * @see org.eclipse.ui.internal.dnd.IDragOverListener#drag(org.eclipse.swt.widgets.Control, java.lang.Object, org.eclipse.swt.graphics.Point, org.eclipse.swt.graphics.Rectangle)
      */
     public IDropTarget drag(Control currentControl, Object draggedObject,
             Point position, final Rectangle dragRectangle) {
         
         // Have to be dragging trim
 if (!(draggedObject instanceof IWindowTrim)) {
             return null;
         }
         
         // OK, we're dragging trim. is it from -this- shell?
 IWindowTrim trim = (IWindowTrim) draggedObject;
         if (trim.getControl().getShell() != windowComposite.getShell()) {
             return null;
         }
         
         // If this is the -first- drag then inform the drop target
 if (dropTarget.draggedTrim == null) {
             dropTarget.startDrag(trim);
         }

         // Forward on to the 'actual' drop target for feedback
 dropTarget.track(position);
         
         // Spin the paint loop after every track
 windowComposite.getDisplay().update();
         
         return dropTarget;
     }
 }

